import { Component, createElement, ErrorInfo, isValidElement } from 'react';
import { ErrorBoundaryProps, FallbackProps } from './types';
type ErrorBoundaryState = { didCatch: boolean; error: any };

const initialState: ErrorBoundaryState = {
    didCatch: false,
    error: null
};

class ErrorBoundaryCore extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
    static getDerivedStateFromError(error: Error) {
        return { didCatch: true, error };
    }
    constructor(props: ErrorBoundaryProps) {
        super(props);
        this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
        this.state = initialState;
    }

    resetErrorBoundary(...args: any[]) {
        const { error } = this.state;

        if (error !== null) {
            this.props.onReset?.({
                args,
                reason: 'imperative-api'
            });

            this.setState(initialState);
        }
    }

    componentDidCatch(error: Error, info: ErrorInfo) {
        this.props.onError?.(error, info);
    }

    componentDidUpdate(prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
        const { didCatch } = this.state;
        const { resetKeys } = this.props;

        if (
            didCatch &&
            prevState.error !== null &&
            hasArrayChanged(prevProps.resetKeys, resetKeys)
        ) {
            this.props.onReset?.({
                next: resetKeys,
                prev: prevProps.resetKeys,
                reason: 'keys'
            });

            this.setState(initialState);
        }
    }

    render() {
        const { children, fallbackRender, fallbackComponent, fallback } = this.props;
        const { didCatch, error } = this.state;

        let childToRender = children;

        if (didCatch) {
            const props: FallbackProps = {
                error,
                resetErrorBoundary: this.resetErrorBoundary
            };

            if (isValidElement(fallback)) {
                childToRender = fallback;
            } else if (typeof fallbackRender === 'function') {
                childToRender = fallbackRender(props);
            } else if (fallbackComponent) {
                childToRender = createElement(fallbackComponent, props);
            } else {
                throw new Error(
                    'react-error-boundary requires either a fallback, fallbackRender, or fallbackComponent prop'
                );
            }
        }

        return childToRender;
    }
}
function hasArrayChanged(a: any[] = [], b: any[] = []) {
    return a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]));
}
export default ErrorBoundaryCore;
